home *** CD-ROM | disk | FTP | other *** search
/ SuperHack / SuperHack CD.bin / CODING / GRAPHICS / FASTSCRL.ZIP / FASTSCRL.TXT
Encoding:
Text File  |  1996-04-26  |  12.8 KB  |  323 lines

  1. Subject: Fast Scrolling in Mode X
  2. Date: 28 Dec 1994 06:52:59 -0500
  3.  
  4. This  article  describes  a  fast  and efficient method of scrolling a
  5. tiled world, using 320x200 Mode X. It will scroll an infinite distance
  6. vertically,  horizontally  and diagonally. On my system, a 386/40 with
  7. an ancient 8-bit video card, it has enough time to bounce  nine  small
  8. (16x10)  sprites  around  on the screen while scrolling at a steady 70
  9. fps.
  10.  
  11. Probably I'll be flamed for posting such kindergarten stuff, but  when
  12. I  took  an  interest  in game programming (not that long ago) I found
  13. such information surprisingly difficult to find.
  14.  
  15. The method described by Diana Gruber in the  Action  Arcade  Adventure
  16. Set scrolls in all directions and is easy to implement. The problem is
  17. that whenever it reaches a tile boundary it  stops  to  copy  a  whole
  18. screenful  of pixels back to the center of the page. On my system this
  19. causes the normal 70 fps rate to miss a beat, producing a slight  jerk
  20. in  the  scrolling. Slowing down the framerate would cure this, but it
  21. still struck me as inefficient to move all those pixels from hither to
  22. yon.
  23.  
  24. I had high hopes for Dave Robert's PC Game Programming Explorer, which
  25. concentrates on such low-level coding. Although  it  is  an  excellent
  26. book in most respects, the scrolling method used is rather limited. It
  27. moves beautifully in the vertical direction and can be easily modified
  28. to scroll horizontally, but it can't go in both directions in the same
  29. game - not unless you are willing to give up page-flipping.
  30.  
  31. This method is derived  from  some  hints  in  the  PCGPE  which  were
  32. confirmed  by  a post here from Henric Steen. In an exchange of e-mail
  33. he gave me a few more pointers... Thanks, Henric.
  34.  
  35. It isn't perfect, though. The big problem  is  that  in  it's  present
  36. version  it is  incompatible with a split screen. If the user wants to
  37. see a status bar s/he will have to request one in a pop-up window,  as
  38. in   the  Commander  Keen  series.  Henric  says  that  it's  easy  to
  39. incorporate a split screen, but despite his help I must admit  that  I
  40. haven't caught on yet... but enough blather, on with the show!
  41.  
  42.  
  43. Not  everyone  uses  the same terminology, so I'll start by explaining
  44. some names I use and a few functions in my Mode X library.
  45.  
  46. ** Definitions **
  47.  
  48. Window - the "window" is the part of vram which is actually  displayed
  49. on the screen. In this method it is always 320x200 pixels.
  50.  
  51. Page  - this is NOT the same as the window. The width of a page is set
  52. by writing to the hardware, but the  starting  point  and  length  are
  53. simply determined by variables in the program.
  54.  
  55. Visible Page - the page in which the window is currently located.
  56.  
  57. Active Page - the page on which we are currently drawing or erasing
  58. sprites.
  59.  
  60. Background Page - holds a copy of the tiled background.
  61.  
  62. Page Flipping - making the active page visible and vice-versa.
  63.  
  64. ** Functions **
  65.  
  66. void window_at( unsigned pg_off, int x, int y )
  67.  
  68. This function writes to the Line Start and HPP registers  to  set  the
  69. visible window at x,y within the page which starts at pg_off.
  70.  
  71. void ltile_to_vram( char *tilearray, int tilenum,
  72.                     unsigned pg_off, int x, int y )
  73.  
  74. Gets a tile from a linear array of tiles and writes it at position x,y
  75. within the specified page.
  76.  
  77. void rect_vram_to_vram( unsigned src_off, unsigned dest_off
  78.                         int x, int y, int hgt, int width )
  79.  
  80. Uses  write mode #1  to copy a rectangular area of pixels from  x,y on
  81. the source page to the same position on the destination page.
  82.  
  83. ** Initialization **
  84.  
  85. By default the length of a Mode X line is 80 addresses, or 320 pixels.
  86. We want a page wide enough so that when the window is  centered  there
  87. is a buffer on each side to hold one column of tiles, which is a total
  88. width of 352 pixels or 88 addresses.
  89.  
  90. The height of a page will be 240 lines, which is 16 + 200 +  24.  When
  91. the window is in the initial position this leave a buffer for one tile
  92. row at the top. An even number of 16x16 tiles won't fit on a  200-line
  93. screen.  After  filling  the screen with 12 rows we have 8 extra lines
  94. hanging off the bottom. The 24-line buffer at the  bottom  allows  for
  95. these 8 lines plus another complete row of tiles.
  96.  
  97. Thus each page takes 240 * 88 = 21120 addresses, and three of them use
  98. 63360 addresses. You can put them adjacent to each other, but I spaced
  99. them approximately evenly in vram.
  100.  
  101. So now we need to create and initialize some global variables:
  102.  
  103. unsigned
  104.    visible_off =   360,    /* initial positions of pages */
  105.    active_off  = 22208,
  106.    back_off    = 44052;
  107.  
  108. int window_x = 16,         /* position of window within page */
  109.     window_y = 16;
  110.  
  111. int world_x = 0,           /* position of upper left tile in world */
  112.     world_y = 0;
  113.  
  114. After  setting  Mode X and the page width, fill all three windows with
  115. the initial background. The tiles will extend all the way  across  the
  116. page  (22  tiles) and down 14 rows - one at the top and 12 1/2 for the
  117. window.
  118.  
  119.   set_mode_x();
  120.   set_page_width( 88 );
  121.   set_pallette( pal );
  122.   window_at( visible_off, window_x, window_y );
  123.  
  124.   for( i=0; i<14; i++ )
  125.      put_tile_row( back_off, i, tiles );
  126.  
  127.   /* copy to other pages */
  128.   rect_vram_to_vram( back_off, active_off,  0, 0, 352, 224 );
  129.   rect_vram_to_vram( back_off, visible_off, 0, 0, 352, 224 );
  130.  
  131. When scrolling North, East or West the method is simple. First  go  as
  132. far as possible by moving the window within the page. When you run out
  133. of valid data move *all* the pages upward or downward  far  enough  to
  134. accommodate a new row or column of tiles.  Grab the tiles, copy to the
  135. other pages and reposition the window within the page.
  136.  
  137. Moving South is just slightly  different,  because  of  the  odd-sized
  138. buffer. To make it easy to grab rows or columns of tiles we would like
  139. to keep the top of the page aligned at a tile boundary. So after using
  140. eight  lines  of the buffer ( ++window_y == 20 ) we grab a new row for
  141. the bottom of the page, but don't reposition  the  pages  until  we've
  142. gone down an even 16 lines ( window_y == 33 ).
  143.  
  144. Meanwhile,  we're  flipping  pages  and handling sprites. With a clean
  145. copy of the tiled background on the background page,  sprites  can  be
  146. erased with a fast block copy, without regard to tile boundaries. This
  147. is more efficient than the "dirty tile" method, which requires copying
  148. a whole tile if a sprite overlaps even a few pixels in the corner.
  149.  
  150. By  now  it  has  probably  occurred to you that we can't do this very
  151. often before one of the pages goes right off the beginning or  end  of
  152. the video segment.
  153.  
  154. If it's the background page, ignore it. The positions of the pages are
  155. held in 16-bit  unsigned ints,  which  will  automaticly  wrap  around
  156. between  the  ends  of  the segment. All of the routines that write or
  157. copy pixels are presumably written in assembly. If you're  using  real
  158. mode  the index registers will also wrap, so that address a000:ffff is
  159. adjacent to a000:0000. In protected mode I believe there's an assembly
  160. directive  to  allow  manipulation  of  the lower 16 bits of the index
  161. registers, so you can have the same effect.
  162.  
  163. If the visible window becomes split, it's a more  serious  matter.  In
  164. most  video  cards  the hardware which paints the screen also wraps at
  165. the ends of the segment, but some SVGA cards don't do that.
  166.  
  167. The solution is simple. We are doing all of this in a loop that  looks
  168. something like this:
  169.  
  170. LOOP:
  171.   wait for retrace
  172.   swap( visible_off, active_off )
  173.   window_at( visible_off, window_x, window_y )
  174.   erase sprites from active page
  175.   check user input
  176.   do scrolling
  177.   if( active_off > 44416 )
  178.       swap( active_off, back_off )
  179.   draw sprites on active page
  180.   goto LOOP
  181.  
  182. Note  that  the  scrolling  is done after the sprites have been erased
  183. >from the active page. At this point the active  and  background  pages
  184. are  pixel-for-pixel  identical,  so  if  the  scrolling has split the
  185. active page, just swap it with the background page. If  the  scrolling
  186. has  split the visible page it won't take effect until the *next* time
  187. it becomes visible. Before that it will take it's turn as  the  active
  188. page, and we'll catch it then.
  189.  
  190. ** Some Improvements **
  191.  
  192. This  is both simple and efficient, but as I've described it so far it
  193. still has one big problem. When it needs more tiles it first  consults
  194. the  world map and copies them one-by-one to the background page. Then
  195. it uses two block copies to transfer them to the other pages,  all  in
  196. one  frame.  That's a lot to do in one frame - in fact it's almost all
  197. the work the engine does. Out of curiosity I temporarily  removed  all
  198. sprite  handling  and the wait-for-retrace function, so it did nothing
  199. but scroll the screen as fast as it could. Under those conditions  the
  200. Watcom profiler said that execution time was distributed as follows:
  201.  
  202. 45.5%  ltile_to_vram()
  203. 49.9%  rect_vram_to_vram()
  204.  4.6%  everything else
  205.  
  206. Each new row or column is copied twice after it is grabbed, so getting
  207. them into vram takes about twice as long as a copy, and  between  them
  208. they account for almost all of the work of the scrolling.
  209.  
  210. So  the  first  15  pixels  require almost no time, then all this gets
  211. dumped into one frame... and that's not the worst. The worst  case  is
  212. when  it's  scrolling  diagonally and the tiles are aligned so that it
  213. needs *both* a new row and column at the same time. Let's tackle these
  214. problems one by one.
  215.  
  216. Do  we  really need to update all the pages at once? No, we don't. The
  217. copy to the visible page can be put off until the next frame, when  it
  218. will be the active page. So instead of doing two copies we can just do
  219. one and set a global variable to indicate that another is needed.  For
  220. instance, when scrolling East:
  221.  
  222.    /* grab new row of tiles */
  223.    put_tile_col( active_off, 21, tiles );
  224.  
  225.    /* copy to other pages */
  226.   rect_vram_to_vram( active_off, back_off, 336, 0, 16, 240 );
  227.   deferred_copy = COPY_RGT;
  228.  
  229. The  deferred  copy can be done right after the page flip, so the main
  230. loop starts like this:
  231.  
  232.   wait for retrace
  233.   swap( visible_off, active_off )
  234.   window_at( visible_off, window_x, window_y )
  235.   if( deferred_copy )  do copy
  236.   erase sprites from active page
  237.  
  238. That's a big improvement, but if we take the percentages of  execution
  239. time  as arbitrary units of time, the worst-case diagonal scroll still
  240. looks like this:
  241.  
  242.             frame   frame+1
  243. Horizontal:   75   |  25   |
  244. Vertical:     75   |  25   |
  245.  
  246. In the vast majority of games the  user  would  never  notice  if  the
  247. screen  moved  a bit horizontally before starting the diagonal scroll,
  248. so by deferring the vertical scroll we could spread it out to  one  of
  249. these:
  250.  
  251. Horizontal:   75  |  25  |  --  |           (better)
  252. Vertical:     --  |  75  |  25  |
  253.  
  254. Horizontal:   75  |  25  |  --  |  --  |    (best)
  255. Vertical:     --  |  --  |  75  |  25  |
  256.  
  257. To  implement  this  requires  some additions to the code. First, when
  258. scrolling diagonally the horizontal scroll must always be done  first.
  259. Then  in  the  vertical  scrolling  functions  we  need  to check if a
  260. deferred copy is pending. If it is, do not import a new row.
  261.  
  262. void scroll_north( void )
  263.   {
  264.    if( --window_y < 0 )
  265.      {
  266.       /* check if we have deferred copy pending */
  267.       if( deferred_copy )
  268.         {
  269.          ++window_y;               /* cancel move */
  270.          return;                   /* wait until next time */
  271.         }
  272.  
  273.    /* get new row, etc */
  274.   }
  275.  
  276. That will do for a one-frame delay, and it can be put off for one more
  277. by defining the flags for the copies with two flags in one int.
  278.  
  279. #define  COPY_TOP  0x11
  280. #define  COPY_BOT  0x21
  281. #define  COPY_LFT  0x31
  282. #define  COPY_RGT  0x41
  283. #define  NOT_YET   0x01
  284.  
  285.   /* in main loop */
  286.   if( deferred_copy )
  287.     {
  288.      switch( deferred_copy )
  289.        {
  290.         case COPY_TOP :
  291.           rect_vram_to_vram( back_off, active_off,  0, 0, 352, 16 );
  292.           break;
  293.  
  294.         /* other cases */
  295.  
  296.         case NOT_YET  :
  297.           deferred_copy = FALSE;
  298.           break;
  299.        }
  300.      deferred_copy &= 0x01;
  301.     }
  302.  
  303. So  the  first  time  through the loop it does the copy and clears all
  304. except the low bit. The second time that is cleared too,  opening  the
  305. door for the vertical scroll.
  306.  
  307. ** The End (finally) **
  308.  
  309. So  there  it  is. I make no claim to be the first to use this method,
  310. not by a long way. It's pretty simple and has probably  been  used  by
  311. umpteen  programmers  for  years.  The  only  problem  is  that nobody
  312. bothered to explain it in any kind of detail, or at least not where  I
  313. could find it.
  314.  
  315. If  anybody  can  think of improvements - especially an elegant way to
  316. add a split screen - I'll be more than happy to hear them. It would be
  317. pushing my luck too far to post the full code for my little demo,  but
  318. if anyone would like it in e-mail, drop me a line.
  319.  
  320.   -]Frank[-
  321.  
  322.  
  323.